iT邦幫忙

2024 iThome 鐵人賽

DAY 18
1
Software Development

可以Go一輩子嗎?系列 第 18

Day18. context與sync在非同步中的應用

  • 分享至 

  • xImage
  •  

Day18. context與sync在非同步中的應用

前言

在現代軟體開發中,並發與非同步操作已成為不可或缺的一部分。Go 語言以其輕量級的 goroutine 和強大的並發處理能力,成為開發者進行並發編程的首選語言。然而,隨著程序的複雜度增加,如何有效地管理和協調多個 goroutine 的執行變得尤為重要。本文將針對初階 Go 開發者,介紹 Go 語言中 contextsync 套件在非同步操作中的應用,重點講解常用方法及其應用場景,並提供簡單易懂的代碼範例。

同步 vs 非同步的比較

在程式設計中,同步(Synchronous)和非同步(Asynchronous)是兩種基本的執行模型。

  • 同步操作:在執行一個同步操作時,程式會等待該操作完成後才繼續執行後續的代碼。例如,當一個函數被調用時,調用者會等待該函數返回結果後才繼續執行。

  • 非同步操作:非同步操作允許程式在等待某個操作完成的同時,繼續執行其他代碼。這在處理 I/O 操作、網路請求等需要等待的任務時特別有用,能夠提升應用程序的效率和響應速度。

在 Go 語言中,goroutine 提供了一種輕量級的方式來實現非同步操作,但隨之而來的是多協程之間的協調和管理問題。這時,contextsync 套件就顯得尤為重要。

為什麼需要控制多協程的執行

當應用程序中存在多個 goroutine 時,控制它們的執行變得非常重要。主要原因包括:

  1. 資源管理:限制同時運行的 goroutine 數量,防止過多的 goroutine 佔用過多資源,導致系統性能下降。

  2. 任務協調:確保多個 goroutine 按照預期的順序執行,避免競爭條件(Race Condition)和死鎖(Deadlock)等問題。

  3. 取消和超時控制:在需要的時候,可以取消不再需要的 goroutine 或設定操作的超時,提升應用的穩定性。

接下來,我們將介紹 contextsync 套件,這兩個套件在控制和協調多 goroutine 的執行方面扮演著重要角色。

context 介紹

context 套件提供了上下文控制的功能,允許我們在不同的 goroutine 之間傳遞取消信號、超時控制和附加值。主要用於控制一組相關的 goroutine,例如在處理 HTTP 請求時,當客戶端取消請求,所有與該請求相關的 goroutine 都應該被取消。

Context 的基本用法

下面是一個使用 context 控制 goroutine 的簡單範例。該範例中,我們創建了一個可以取消的上下文,並在另一個 goroutine 中監聽取消信號,以便在需要時取消操作。

package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    // 創建一個可取消的上下文
    ctx, cancel := context.WithCancel(context.Background())

    // 啟動一個 goroutine,模擬長時間運行的操作
    go func() {
        for {
            select {
            case <-ctx.Done():
                fmt.Println("Goroutine 被取消")
                return
            default:
                fmt.Println("Goroutine 正在運行")
                time.Sleep(500 * time.Millisecond)
            }
        }
    }()

    // 主程式等待 2 秒後取消上下文
    time.Sleep(2 * time.Second)
    cancel()

    // 等待 goroutine 完成
    time.Sleep(1 * time.Second)
    fmt.Println("主程式結束")
}

說明

  1. context.WithCancel 創建了一個可取消的上下文 ctx,並返回一個取消函數 cancel
  2. goroutine 中的 select 語句會監聽 ctx.Done(),當上下文被取消時,會接收到取消信號並結束 goroutine。
  3. 主程式等待 2 秒後調用 cancel(),取消上下文,從而停止 goroutine 的運行。

常用的 Context 方法

  • context.Background():返回一個空的上下文,通常作為所有上下文的根。
  • context.WithCancel(parent):基於父上下文創建一個可取消的上下文。
  • context.WithTimeout(parent, timeout):基於父上下文創建一個具有超時的上下文,超時後自動取消。
  • context.WithValue(parent, key, value):基於父上下文創建一個帶有鍵值對的上下文,用於在 goroutine 之間傳遞值。

Context.WithTimeout 的使用範例

context.WithTimeout 可以在指定的時間內自動取消上下文,非常適合用於需要設置操作超時的場景。

package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    // 設定一個 1 秒的超時上下文
    ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
    defer cancel()

    // 啟動一個 goroutine,模擬長時間運行的操作
    go func() {
        select {
        case <-time.After(2 * time.Second):
            fmt.Println("Goroutine 完成工作")
        case <-ctx.Done():
            fmt.Println("Goroutine 被取消:", ctx.Err())
        }
    }()

    // 等待 goroutine 完成或超時
    time.Sleep(3 * time.Second)
    fmt.Println("主程式結束")
}

說明

  1. 使用 context.WithTimeout 創建了一個在 1 秒後自動取消的上下文。
  2. goroutine 中的 select 會等待 2 秒後完成工作,但由於上下文在 1 秒後被取消,會先接收到取消信號並終止操作。

sync 介紹

sync 套件提供了多種同步原語,用於在多 goroutine 之間協調操作,確保數據的一致性和正確性。常用的同步工具包括互斥鎖(Mutex)、等待組(WaitGroup)、條件變量(Cond)等。

WaitGroup 的使用

sync.WaitGroup 用於等待一組 goroutine 完成。這在需要等待多個並行操作完成後再繼續執行下一步時非常有用。

下面是一個使用 WaitGroup 的簡單範例,演示如何等待多個 goroutine 完成:

package main

import (
    "fmt"
    "sync"
    "time"
)

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done()
    fmt.Printf("Worker %d 開始工作\n", id)
    time.Sleep(time.Second)
    fmt.Printf("Worker %d 完成工作\n", id)
}

func main() {
    var wg sync.WaitGroup

    // 啟動 3 個 goroutine
    for i := 1; i <= 3; i++ {
        wg.Add(1)
        go worker(i, &wg)
    }

    // 等待所有 goroutine 完成
    wg.Wait()
    fmt.Println("所有工作完成")
}

說明

  1. 創建一個 WaitGroup 變量 wg
  2. 在啟動每個 goroutine 前調用 wg.Add(1),表示有一個 goroutine 需要等待。
  3. 在每個 goroutine 完成工作時調用 wg.Done(),表示該 goroutine 已完成。
  4. 主程式通過 wg.Wait() 等待所有 goroutine 完成後再繼續執行。

互斥鎖(Mutex)的使用

sync.Mutex 用於保護共享資源,防止多個 goroutine 同時訪問導致的數據競爭。

以下是一個使用 Mutex 保護共享變量的範例:

package main

import (
    "fmt"
    "sync"
)

func main() {
    var (
        mu    sync.Mutex
        count int
        wg    sync.WaitGroup
    )

    // 啟動 1000 個 goroutine 增加 count
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            mu.Lock()
            count++
            mu.Unlock()
        }()
    }

    wg.Wait()
    fmt.Printf("最終 count 的值為 %d\n", count)
}

說明

  1. 創建一個 Mutex 變量 mu,用於保護共享變量 count
  2. 每個 goroutine 在增加 count 前先調用 mu.Lock() 鎖定,操作完成後調用 mu.Unlock() 解鎖。
  3. 使用 WaitGroup 等待所有 goroutine 完成,確保 count 的最終值正確。

RWMutex 的使用

sync.RWMutex 是一種讀寫互斥鎖,允許多個讀操作同時進行,但寫操作需要獨佔鎖。

package main

import (
    "fmt"
    "sync"
    "time"
)

type SafeMap struct {
    mu sync.RWMutex
    m  map[string]int
}

func (s *SafeMap) Read(key string) (int, bool) {
    s.mu.RLock()
    defer s.mu.RUnlock()
    val, ok := s.m[key]
    return val, ok
}

func (s *SafeMap) Write(key string, value int) {
    s.mu.Lock()
    defer s.mu.Unlock()
    s.m[key] = value
}

func main() {
    s := SafeMap{m: make(map[string]int)}
    var wg sync.WaitGroup

    // 寫入數據
    wg.Add(1)
    go func() {
        defer wg.Done()
        for i := 0; i < 5; i++ {
            s.Write(fmt.Sprintf("key%d", i), i)
            time.Sleep(100 * time.Millisecond)
        }
    }()

    // 讀取數據
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            val, ok := s.Read(fmt.Sprintf("key%d", id))
            if ok {
                fmt.Printf("讀取到 key%d: %d\n", id, val)
            } else {
                fmt.Printf("key%d 不存在\n", id)
            }
        }(i)
    }

    wg.Wait()
    fmt.Println("所有操作完成")
}

說明

  1. 定義了一個 SafeMap 結構,使用 RWMutex 保護內部的 map。
  2. Read 方法使用 RLock 進行讀操作,允許多個讀操作同時進行。
  3. Write 方法使用 Lock 進行寫操作,確保寫操作的獨佔性。
  4. 在主函數中,同時啟動寫入和讀取的 goroutine,確保數據的安全訪問。

那麼今天的文章就到這告一段落,如果我的文章有任何地方有錯誤請在留言區反應
明天將會介紹Go語言的Package的基本概念及創建方法
time

REF


上一篇
Day17. select語句及其在非同步中的應用
下一篇
Day19. Package的基本概念及創建方法
系列文
可以Go一輩子嗎?31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言